/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
 */

import Buffer from 'buffer';
import crypto, { createHash, randomBytes } from 'crypto';
import fs from 'fs';
import path from 'path';

import fse from 'fs-extra';

import { coreParameter } from '../../internal/data/global-core-parameters.js';
import { HvigorLogger } from '../../log/hvigor-log.js';
import {
  DAEMON_ROOT_KEY_COMPONENT_LENGTH,
  DAEMON_SALT_KEY_LENGTH,
  DAEMON_WORK_KEY_LENGTH,
  DEFAULT_DAEMON_META_PATH,
  KEY_FILE_DIRECTORY_PERMISSIONS,
  KEY_FILE_PERMISSIONS,
} from '../const/daemon-const.js';

const log = HvigorLogger.getLogger('SessionIdHelper');

/**
 * SessionId加解密相关操作的工具类
 *
 * @since 2023/5/23
 */
export class SessionIdHelper {
  private static dirs: string[] = ['fd', 'ac', 'ce'];

  // mac 访达文件夹 默认生成.DS_Store 文件
  private static macDSStore = '.DS_Store';

  private static component = new Int8Array([0x31, 0xf3, 0x09, 0x73, 0xd6, 0xaf, 0x5b, 0xb8, 0xd3, 0xbe, 0xb1, 0x58, 0x65, 0x83, 0xc0, 0x77]);
  private static key = SessionIdHelper.getKey(DEFAULT_DAEMON_META_PATH, log);

  /**
   * 加密
   *
   * @param msg 明文
   * @param log HvigorLogger
   * @return string 密文
   */
  static encryptPwdByDefaultMaterial(msg: string, log: HvigorLogger): string {
    return SessionIdHelper.encryptPwd(msg, log);
  }

  /**
   * 使用aes-gcm算法加密消息
   * AesGcmKit，其实现符合《密码算法应用规范 V1.4.docx》中针对于AES-GCM的使用要求
   * 填充参数是NoPadding，密钥长度16 byte(128 bit)，iv长度12 byte(96bit)，iv每次都是采用SecureRandom生成，
   * tag参数长度16 byte(128 bit)。
   *
   * @param msg 密码明文
   * @param log HvigorLogger
   * @return string
   */
  static encryptPwd(msg: string, log: HvigorLogger): string {
    let result: Buffer.Buffer = Buffer.Buffer.from('');
    try {
      const msgBytes = new Int8Array(Buffer.Buffer.from(msg));
      result = SessionIdHelper.encrypt(SessionIdHelper.key, msgBytes);
    } catch (error: any) {
      log.printErrorExit('SECTION_ID_ERROR', ['Encrypt failed']);
      if (coreParameter.startParams.printStackTrace && error.stack) {
        log.printErrorExit('SECTION_ID_ERROR', [error.stack]);
      }
    }
    return result.toString('hex');
  }

  /**
   * 根据密钥加密, 获取密文密码
   * 被加密后的数据格式如下
   * 其中content数据中包含tag数据
   * -------------------------------------------------------------------------------
   * | content length (4byte) | iv key (12 byte) | encrypted | auth tag ( 16 byte ) |
   *  ------------------------------------------------------------------------------
   *
   * @param key 密钥
   * @param msg 明文
   * @private Buffer.Buffer
   */
  private static encrypt(key: Int8Array, msg: Int8Array) {
    const ivKey: Int8Array = new Int8Array(randomBytes(12));

    const cipherGCM = crypto.createCipheriv('aes-128-gcm', key, ivKey);
    const update = cipherGCM.update(msg);
    const final = cipherGCM.final();
    const authTag = new Int8Array(cipherGCM.getAuthTag());
    const content = new Int8Array(Buffer.Buffer.concat([update, final]));
    const length = content.length + authTag.length;
    const data: Int8Array = new Int8Array(4 + ivKey.length + content.length + authTag.length);
    data[0] = (length >> 24) & 255;
    data[1] = (length >> 16) & 255;
    data[2] = (length >> 8) & 255;
    data[3] = length & 255;
    for (let i = 0; i < ivKey.length; i++) {
      data[i + 4] = ivKey[i];
    }
    for (let i = 0; i < content.length; i++) {
      data[i + 16] = content[i];
    }
    const index = 4 + ivKey.length + content.length;
    for (let i = 0; i < authTag.length; i++) {
      data[i + index] = authTag[i];
    }
    return Buffer.Buffer.from(data);
  }

  /**
   * 解密
   *
   * @param ciphertext 密文
   * @param log HvigorLogger
   * @return string 明文
   */
  static decryptPwdByDefaultMaterial(ciphertext: string, log: HvigorLogger): string {
    return SessionIdHelper.decryptPwd(ciphertext, log);
  }

  /**
   * 使用aes-gcm算法解密消息
   * AesGcmKit，其实现符合《密码算法应用规范 V1.4.docx》中针对于AES-GCM的使用要求
   * 填充参数是NoPadding，密钥长度16 byte(128 bit)，iv长度12 byte(96bit)，iv每次都是采用SecureRandom生成，tag参数长度16 byte(128 bit)。
   *
   * @param encryptedPwd 加密密码
   * @param log HvigorLogger
   * @return string
   */
  static decryptPwd(encryptedPwd: string, log: HvigorLogger): string {
    let result: Buffer.Buffer = Buffer.Buffer.from('');
    try {
      const msgBytes = new Int8Array(Buffer.Buffer.from(encryptedPwd, 'hex'));
      result = SessionIdHelper.decrypt(SessionIdHelper.key, msgBytes);
    } catch (error: any) {
      log.printErrorExit('SECTION_ID_ERROR', ['Decryption failed']);
      if (coreParameter.startParams.printStackTrace && error.stack) {
        log.printErrorExit('SECTION_ID_ERROR', [error.stack]);
      }
    }
    return result.toString('utf-8');
  }

  /**
   * 根据密钥解密, 获取明文密码
   * 被加密后的数据格式如下
   * 其中content数据中包含tag数据
   * -------------------------------------------------------------------------------
   * | content length (4byte) | iv key (12 byte) | encrypted | auth tag ( 16 byte ) |
   *  ------------------------------------------------------------------------------
   *
   * @param key 密钥
   * @param msg 加密密码
   * @private Buffer.Buffer
   */
  private static decrypt(key: Int8Array, msg: Int8Array) {
    const bodyLength = ((msg[0] & 0xff) << 24) | ((msg[1] & 0xff) << 16) | ((msg[2] & 0xff) << 8) | (msg[3] & 0xff);
    const ivLength = msg.length - 4 - bodyLength;
    const ivKey = msg.slice(4, 4 + ivLength);
    const decipherGCM = crypto.createDecipheriv('aes-128-gcm', key, ivKey);
    const authTag = msg.slice(msg.length - 16);
    decipherGCM.setAuthTag(authTag);

    const update = decipherGCM.update(msg.subarray(4 + ivLength, msg.length - 16));
    const final = decipherGCM.final();
    return Buffer.Buffer.concat([update, final]);
  }

  /**
   * 获取解密密钥
   *
   * @param materialDir 签名材料地址
   * @param log HvigorLogger
   * @private Int8Array
   */
  private static getKey(materialDir: string, log: HvigorLogger): Int8Array {
    // if not exists ,create file
    if (!fs.existsSync(DEFAULT_DAEMON_META_PATH)) {
      SessionIdHelper.createMaterial(log);
    }

    if (!fs.statSync(DEFAULT_DAEMON_META_PATH).isDirectory()) {
      log.printErrorExit('SECTION_ID_ERROR', ['No materials.']);
    }
    const files = fs.readdirSync(DEFAULT_DAEMON_META_PATH);
    if (files.length === 0) {
      log.printErrorExit('SECTION_ID_ERROR', ['No materials.']);
    }
    this.dirs.forEach((dirName) => {
      if (!files.includes(dirName)) {
        log.printErrorExit('SECTION_ID_ERROR', [`Can not find signing material ${dirName}`]);
      }
    });
    const components = SessionIdHelper.readFd(path.resolve(DEFAULT_DAEMON_META_PATH, this.dirs[0]), log);
    const salt = SessionIdHelper.readSalt(path.resolve(DEFAULT_DAEMON_META_PATH, this.dirs[1]), log);
    const rootKey = SessionIdHelper.getRootKey(components, salt, log);
    const workMaterial = SessionIdHelper.readWorkMaterial(path.resolve(DEFAULT_DAEMON_META_PATH, this.dirs[2]), log);
    return new Int8Array(SessionIdHelper.decrypt(rootKey, workMaterial));
  }

  /**
   * PBKDF2WithHmacSHA256迭代10000次生成rootKey
   *
   * @param components
   * @param salt
   * @param log
   * @private
   */
  private static getRootKey(components: Int8Array[], salt: Int8Array, log: HvigorLogger) {
    const full = components.concat(SessionIdHelper.component);
    const finalComponents = SessionIdHelper.xorComponents(full, log);
    const buffer = crypto.pbkdf2Sync(finalComponents.toString(), salt, 10000, 16, 'sha256');
    return new Int8Array(buffer);
  }

  private static xorComponents(components: Int8Array[], log: HvigorLogger) {
    components.forEach((value) => {
      if (value.length !== 16) {
        log.printErrorExit('SECTION_ID_ERROR', ['Signing material data error!']);
      }
    });
    let result = SessionIdHelper.xor(components[0], components[1], log);
    for (let i = 2; i < components.length; i++) {
      result = SessionIdHelper.xor(result, components[i], log);
    }
    return Buffer.Buffer.from(result);
  }

  /**
   * 异或
   *
   * @param b1
   * @param b2
   * @param log
   * @private
   */
  private static xor(b1: Int8Array, b2: Int8Array, log: HvigorLogger): Int8Array {
    if (b1.byteLength !== b2.byteLength) {
      log.printErrorExit('SECTION_ID_ERROR', ['Signing material data error!']);
    }
    const result: Int8Array = new Int8Array(b1.byteLength);
    for (let i = 0; i < b1.byteLength; i++) {
      result[i] = b1[i] ^ b2[i];
    }
    return result;
  }

  /**
   * 读取./material/fd
   *
   * @param dir
   * @param log
   * @private
   */
  private static readFd(dir: string, log: HvigorLogger) {
    if (!fs.existsSync(dir)) {
      log.printErrorExit('SECTION_ID_ERROR', ['Material not found!']);
    }
    const files = fs.readdirSync(dir).filter((file) => file != SessionIdHelper.macDSStore);
    if (files.length !== 3) {
      log.printErrorExit('SECTION_ID_ERROR', ['Signing material is illegal!']);
    }
    const result: Int8Array[] = [];
    files.forEach((fileName) => result.push(SessionIdHelper.readDirBytes(path.resolve(dir, fileName), log)));
    return result;
  }

  /**
   * 读取盐值./material/ac
   *
   * @param dir
   * @param log
   * @private
   */
  private static readSalt(dir: string, log: HvigorLogger): Int8Array {
    return SessionIdHelper.readDirBytes(dir, log);
  }

  /**
   * workMaterial ./material/ce
   *
   * @param dir
   * @param log
   * @private
   */
  private static readWorkMaterial(dir: string, log: HvigorLogger) {
    return SessionIdHelper.readDirBytes(dir, log);
  }

  /**
   * 读取目录中文件 默认一个文件
   *
   * @param dir
   * @param log
   * @private
   */
  private static readDirBytes(dir: string, log: HvigorLogger): Int8Array {
    if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
      log.printErrorExit('SECTION_ID_ERROR', [`Can not find the signing material: ${dir}`]);
    }
    const files = fs.readdirSync(dir).filter((file) => file != SessionIdHelper.macDSStore);
    if (files.length !== 1) {
      log.printErrorExit('SECTION_ID_ERROR', ['Signing material error']);
    }
    const buffer = fs.readFileSync(path.resolve(dir, files[0]));
    return new Int8Array(buffer);
  }

  private static createMaterial(log: HvigorLogger) {
    log.debug('begin to create new key files');
    const metaDir = path.join(DEFAULT_DAEMON_META_PATH);
    if (!fse.existsSync(metaDir)) {
      fse.mkdirsSync(metaDir);
    }

    // 秘钥文件目录 权限为 0o700
    fs.chmodSync(path.join(metaDir), KEY_FILE_DIRECTORY_PERMISSIONS);

    const rkDir = path.join(metaDir, this.dirs[0]);
    const c0 = SessionIdHelper.createAndStoreKey(path.join(rkDir, '0'), DAEMON_ROOT_KEY_COMPONENT_LENGTH);
    const c1 = SessionIdHelper.createAndStoreKey(path.join(rkDir, '1'), DAEMON_ROOT_KEY_COMPONENT_LENGTH);
    const c2 = SessionIdHelper.createAndStoreKey(path.join(rkDir, '2'), DAEMON_ROOT_KEY_COMPONENT_LENGTH);
    const salt = SessionIdHelper.createAndStoreKey(path.join(metaDir, this.dirs[1]), DAEMON_SALT_KEY_LENGTH);
    const rootKey = SessionIdHelper.getRootKey([c0, c1, c2], salt, log);
    SessionIdHelper.createAndStoreEnKey(rootKey, path.join(metaDir, this.dirs[2]));
  }

  private static createAndStoreKey(fileDir: string, keyLength: number): Int8Array {
    // need hash or not
    const key = new Int8Array(randomBytes(16));
    const hash = createHash('sha256').update(key).digest('hex');

    if (!fse.existsSync(fileDir)) {
      fse.mkdirpSync(fileDir);
    }

    const filePath = path.join(fileDir, hash);
    fs.writeFileSync(filePath, key);

    // 修改文件权限为 0o600
    fse.chmodSync(filePath, KEY_FILE_PERMISSIONS);
    return key;
  }

  private static createAndStoreEnKey(rootKey: Int8Array, ceDir: string) {
    const workKey = new Int8Array(randomBytes(DAEMON_WORK_KEY_LENGTH));
    const encWorkKey = SessionIdHelper.encrypt(rootKey, workKey);
    const hash = createHash('sha256').update(encWorkKey).digest('hex');

    if (!fse.existsSync(ceDir)) {
      fse.mkdirpSync(ceDir);
    }
    const filePath = path.join(ceDir, hash);
    fs.writeFileSync(filePath, encWorkKey);

    // 修改文件权限为 0o600
    fs.chmodSync(filePath, KEY_FILE_PERMISSIONS);
  }
}
